[Issue #799] Transform PoC#810
Conversation
| def to_common(native: Any) -> TransformResult[Any]: | ||
| try: | ||
| result = transform_from_mapping(native, to_common_mapping, handlers=merged) | ||
| # TODO (full SDK): run model_validate on result and append validation |
There was a problem hiding this comment.
I'm not sure if we should handle this TODO here or later down the road. Would love thoughts on this.
|
🚀 Website Preview Deployed! Preview your changes at: https://cg-pr-810.billy-daly.workers.dev This preview will be automatically deleted when the PR is closed. |
| to_common_mapping: dict[str, Any], | ||
| from_common_mapping: dict[str, Any], | ||
| handlers: dict[str, Handler] | None = None, | ||
| common_model: type[BaseModel] | None = None, |
There was a problem hiding this comment.
Calling this out as a needed change from the existing ADR. Without having the common_model in the signature we don't have a way to validate an extended class that contains optional fields. @SnowboardTechie for awareness
There was a problem hiding this comment.
Nice work on round 2. 7 of my 9 earlier findings are fully addressed, and the other 2 are documented-as-intended. The HandlerError refactor in particular reads much cleaner than the hardcoded handler=None we started with.
One latent bug worth surfacing before this ships: transform_node silently treats handler keys as literals when a non-handler sibling iterates first. The fact that build_transforms and transform_from_mapping are both public surface now makes it more load-bearing than before. Reproducer in the inline on utils/transformation.py.
Two small follow-ups on the round-2 fix itself, both flagged on individual threads: HandlerError is a behavior change for dump_with_mapping / validate_with_mapping callers (worth deciding whether to make it ValueError-compatible), and the new attribution path is not covered by an assertion in the exception test.
SnowboardTechie
left a comment
There was a problem hiding this comment.
Round-3 looks great! The ADR has some drift against #803 in the merge_extensions / filters area, but IMO we should rebase main onto current HOLD-transforms after this merges.
Approving w/ 2 small suggests: git rm lib/python-sdk/.coverage and add .coverage to either lib/python-sdk/.gitignore or the root. It looks like Pytest-cov SQLite output landed in the tree. And another trimming an extra /.
There was a problem hiding this comment.
Not sure we want this commited?
Co-authored-by: Bryan Thompson <18094023+SnowboardTechie@users.noreply.github.com>
Port the Python transforms PoC (PR #810, branch 799-transform-poc-fetch) to @common-grants/sdk so the ADR-0022 / ADR-0017 contract is validated in both SDKs before either is locked in for full implementation. Public additions under @common-grants/sdk/extensions: - buildTransforms() — compile a pair of ADR-0017 mapping objects into typed (toCommon, fromCommon) callables with call-time structural validation. Optional commonModel Zod schema turns parse failures into PluginError[] rather than thrown exceptions. - TransformResult<T> — unconditional { result, errors } return shape (ADR-0022 Decision #7). - PluginError — structured error class with path / handler / sourceValue / cause (ADR-0022 Decision #9). Docstring documents the PII surface on both sourceValue (carries the full input record) and message (data-bearing on the Zod-validation path because Zod's default error map embeds received values). - transformFromMapping(), getFromPath(), DEFAULT_HANDLERS — mapping runtime; six built-in handlers (const, field, match, switch alias, numberToString, stringToNumber). - definePlugin() accepts optional meta and transformSchemas. Existing callers passing only `extensions` are unaffected. Security hardening (mapping JSON may be reconstituted from untrusted sources via mergeExtensions(), so the runtime must fail loud on hostile shapes): - buildTransforms() rejects custom handler names that collide with the default registry or shadow Object.prototype keys (constructor, toString, __proto__, etc.) at call time. - validateMapping() rejects `__proto__` as an output field name at build time; transformFromMapping() rejects it again at walk time so the JSON attack vector (own-enumerable __proto__ key from JSON.parse) fails fast in both places. - stringToNumber's error message does not embed the source value (would flow into PluginError.message and bypass the sourceValue PII guard). Out of scope (matches Python PoC; deferred to full SDK): - Auto-generation of transforms from declarative extensions.schemas[obj].mappings inside definePlugin() (Decision #6 TODO). - Always-on commonModel validation inside definePlugin() — opt-in at buildTransforms() for now (Decision #7 TODO). Includes: - examples/transforms.ts round-trip (pnpm example:transforms) - README "Plugin transformations" section + API reference table - 5 new define-plugin specs, 30 transformation-handler specs, 12 buildTransforms specs (427 tests total, all passing) - Minor changeset bump for @common-grants/sdk Targets HOLD-transforms per the SDK Plugin Enhancements branching strategy.
|
🗑️ Preview Cleaned Up The preview for this PR has been automatically deleted. |
Summary
Changes proposed
Implements a bidirectional transform framework as a proof-of-concept for the plugin system. Plugin authors can now define declarative mapping dicts that compile into
to_common/from_commoncallables, enabling data translation between a source system's native format and the CommonGrants format without writing imperative transform code.New utilities:
build_transforms()— compiles a pair of mapping dicts into typed(to_common, from_common)callables with structural validation at call timeTransformResult— unconditional return shape carrying aresultdict and a list ofPluginErrors (non-fatal; partial result always returned)PluginError— structured error type withpath,handler,source_value, andcausefields for programmatic error handlingObjectSchemasInput— bundlesto_common/from_commonfor a single object type; passed todefine_plugin()viatransform_schemasPluginMeta— optional metadata attached to a plugin (name,version,source_system,capabilities)Built-in mapping handlers (in
utils/transformation.py):fieldconstmatchswitchmatch(backward compat)numberToStringstringToNumberCustom handlers can be registered per
build_transforms()call and cannot override built-in names.Sample grants.gov plugin (
examples/plugins/grants_gov/): demonstrates the full interface — bidirectional field mapping, status case conversion, funding amounts, key dates, andplugin metadata.
Documentation (
extensions/README.md): new "Bidirectional Transforms" section covering mapping format, handler reference, custom handlers, and roundtrip usage.Tests (
tests/extensions/,tests/utils/): coverage forbuild_transforms,TransformResult,PluginError,ObjectSchemasInput,PluginMeta, andtransform_from_mapping.Context for reviewers
This is a proof-of-concept scoped to the transform layer only. It is intentionally limited:
build_transforms()validates mapping structure at call time but cannot validate that field paths resolve against real data (deferred to full SDK with schema introspection).matchare not reversible.TODO (full SDK)comments intransforms.pycall out wheremodel_validateintegration belongs (indefine_plugin(), which knows the target Pydantic model).Verification:
Run the working example end-to-end: